package com.wx.forum.service.impl;

import com.wx.forum.common.AppResult;
import com.wx.forum.common.ResultCode;
import com.wx.forum.contant.Contants;
import com.wx.forum.dao.ArticleMapper;
import com.wx.forum.exception.ApplicationException;
import com.wx.forum.model.Article;
import com.wx.forum.model.Board;
import com.wx.forum.model.User;
import com.wx.forum.service.IArticleService;
import com.wx.forum.service.IAuthService;
import com.wx.forum.utils.StringUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;

@Service
@Slf4j
public class ArticleServicelmpl implements IArticleService {
    // 线程安全队列，key=帖子ID，value=这段时间的净变化
    private final BlockingQueue<Map.Entry<Long,Integer>> likeFlushQueue =
            new LinkedBlockingQueue<>();
    @Resource(name = "likeExecutor")
    private Executor likeExecutor;   // 这里是对象，不是字符串
    private final Object flushLock = new Object();

    @Resource
    private ArticleMapper articleMapper;
    @Resource
    private BoardServiceImpl boardService;
    @Resource
    private UserServiceImpl userService;
    @Autowired
    private IAuthService authService;
    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;


    @Override
    public void creat(Article article) {
        //判空
        if (article == null || article.getBoardId() == null
                || article.getUserId() == null || StringUtil.isEmpty(article.getContent())
                || StringUtil.isEmpty(article.getTitle())) {

            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        //设置默认值
        article.setVisitCount(0);
        article.setReplyCount(0);
        article.setLikeCount(0);
        article.setState((byte) 0);
        article.setDeleteState((byte) 0);
        Date date = new Date();
        article.setCreateTime(date);
        article.setUpdateTime(date);
        //插入帖子
        int raw = articleMapper.insertSelective(article);
        if (raw <= 0) {
            log.warn(ResultCode.FAILED_CREATE.toString());
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_CREATE));
        }
        //更新用户的贴子数
        //1.查询用户
        User user = userService.selectById(article.getUserId());
        if (user == null) {
            log.warn(ResultCode.ERROR_IS_NULL.toString());
            throw new ApplicationException(AppResult.failed(ResultCode.ERROR_IS_NULL));
        }
        //2.贴子数加1
        userService.addOneArtcleCountByUserId(user.getId());

        //更新板块贴子数
        //1.查询板块
        Board board = boardService.selectById(article.getBoardId());
        if (board == null) {
            log.warn(ResultCode.ERROR_IS_NULL.toString());
            throw new ApplicationException(AppResult.failed(ResultCode.ERROR_IS_NULL));
        }
        //2.贴子数加1
        boardService.addOneArticleCountByBoardId(board.getId());

        log.info("用户" + user.getUsername() + "发帖成功," + "articleId: " + article.getId());
        //当完成插入帖子操作后，数据库会自动把帖子的id返回给插入的article对象
        //因为添加了  useGeneratedKeys="true" keyProperty="id"
        //throw new ApplicationException(AppResult.failed("测试回滚"));

        //创建帖子点赞数键值对
        redisTemplate.opsForValue().set(Contants.THUMBS_KEY+article.getId(),"0");
    }

    /**
     * 查询所有的合法帖子
     *
     * @return
     */
    @Override
    public List<Article> selectAll() {

        return articleMapper.selectAll();
    }

    @Override
    public List<Article> selectAllByBoardId(Long boardId) {
        //1.判空
        if (boardId == null || boardId <= 0) {
            //打印信息
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        //2.判断板块是否存在
        Board board = boardService.selectById(boardId);
        if (board == null) {
            //打印信息
            log.warn(ResultCode.FAILED_NOT_EXISTS.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_NOT_EXISTS));
        }
        //3.调用DAO，查询列表
        List<Article> articleList = articleMapper.selectAllByBoardId(boardId);
        return articleList;
    }

    @Override
    public Article selectDetailById(Long id) {
        //1.参数校验
        if (id == null || id <= 0) {
            //打印信息
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }

        //2.查询帖子详情
        Article article = articleMapper.selectDetailById(id);
        if (article == null) {
            log.warn(ResultCode.FAILED_NOT_EXISTS.toString());
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_NOT_EXISTS));

        }
        String likeCount = (String) redisTemplate.opsForValue().get(Contants.THUMBS_KEY + article.getId());
        article.setLikeCount(Integer.parseInt(likeCount));

        //3.更新访问量
        Article updateArticle = new Article();
        updateArticle.setId(article.getId());
        updateArticle.setVisitCount(article.getVisitCount() + 1);
        int row = articleMapper.updateByPrimaryKeySelective(updateArticle);
        if (row != 1) {
            log.warn(ResultCode.ERROR_SERVICES.toString());
            throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));
        }
        //4.返回数据
        //更新返回数据的帖子访问量
        article.setVisitCount(article.getVisitCount() + 1);
        return article;
    }

    @Override
    public Article selectById(Long id) {
        //1.参数校验
        if (id == null || id <= 0) {
            //打印信息
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        Article article = articleMapper.selectByPrimaryKey(id);
        return article;
    }

    @Override
    public void modify(Long id, String content, String title) {
        //1.参数校验
        if (id == null || id <= 0 || StringUtil.isEmpty(content)
                || StringUtil.isEmpty(title)) {
            //打印信息
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        //2.构建更新对象
        Article updateArticle = new Article();
        updateArticle.setId(id);
        updateArticle.setContent(content);
        updateArticle.setTitle(title);
        updateArticle.setUpdateTime(new Date());
        //3.调用dao
        int row = articleMapper.updateByPrimaryKeySelective(updateArticle);
        if (row != 1) {
            log.warn(ResultCode.ERROR_SERVICES.toString());
            throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));
        }
    }
    /**
     * 点赞帖子
     * @param userId
     * @param articleId
     */

    @Override
    public Integer thumbsUpById(Long userId,Long articleId,Integer isLike) {

        //1.参数校验
        if (articleId == null || articleId <= 0) {
            //打印信息
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        //2.查询修改对象
        Article article = articleMapper.selectByPrimaryKey(articleId);
        if (article == null) {
            log.warn(ResultCode.FAILED_NOT_EXISTS.toString());
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_NOT_EXISTS));
        }
        //校验帖子状态
        if(article.getState() == 1 || article.getDeleteState() == 1){
            log.warn(ResultCode.FAILED.toString());
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED));
        }
        System.out.println("isLike:" + isLike);
        //点赞关系键
        String key = userId + "::" + articleId;
        //帖子点赞数键
        String cntKey = Contants.THUMBS_KEY +articleId;
        String delta   = isLike > 0 ? "1" : "0"; //点赞还是取消
        //比较点赞关系，并修改点赞数
        Integer newCnt = cau(key,cntKey,delta);
        // 异步批量刷库（只记录净变化，已有代码继续用）
        if(newCnt != null)
        offerQueue(articleId,isLike);

        //还要干的事
        //1.isLike怎么传 true
        //2.前端接口要修改 true
        //3.查询帖子列表要修改 true
        //4.存储点赞关系 true
        //5.刷库保证线程安全 true
        //6.数据一致性 true
        return newCnt;
    }

    /**
     * 点赞刷库，减小数据库压力
     */
    @Async("likeExecutor")   // Spring 线程池
    private void flushDB(){
        System.out.println("刷库开始");

            int retry = 3;
            while( retry-- > 0){
                try {
                    List<Map.Entry<Long, Integer>> list = new ArrayList<>();
                    int drained = likeFlushQueue.drainTo(list, 500);
                    System.out.println("drainTo返回值=" + drained);
                    // 同帖子聚合为净变化
                    Map<Long, Integer> batch = new HashMap<>();
                    for (Map.Entry<Long, Integer> e : list) {
                        batch.merge(e.getKey(), e.getValue(), Integer::sum);
                    }
                    // 每条只执行一次原子 UPDATE
                    batch.forEach((articleId, value) -> {
                        if (value != 0) {
                            articleMapper.updateLikeCount(articleId, value); // +net 或 -net
                        }
                    });
                    System.out.println("刷库结束");
                    return;
                } catch (Exception e) {
                    log.warn("刷库重试 {}", 3 - retry, e);
                }
            }

    }

    /**
     * 每次 offer 后检查 每攒够50条就执行刷库
     * @param articleId
     * @param delta
     */
    //
    public void offerQueue(Long articleId, int delta) {
        likeFlushQueue.offer(Map.entry(articleId, delta));
        if (likeFlushQueue.size() >= 250) {          // 攒够 250 条立即刷
            CompletableFuture.runAsync(this::flushDB, likeExecutor);
        }
    }
    /**
     * 每 20 秒兜底刷（防止 <250 积压）并且尽量保证数据库和redis点赞数一致
     */
    @Scheduled(fixedDelay = 20_000)
    public void scheduledFlush() {
        System.out.println("定时刷库");

        if (!likeFlushQueue.isEmpty()) {
            System.out.println("定时刷赞");
            CompletableFuture.runAsync(this::flushDB, likeExecutor);
        }
    }


    @Transactional //使用多条sql，要加事务
    @Override
    public void deleteById(Long id) {
        //1.参数校验
        if (id == null || id <= 0) {
            //打印信息
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        //2.更新帖子信息
        //2.1查新帖子信息
        Article article = articleMapper.selectByPrimaryKey(id);
        if(article == null || article.getDeleteState() == 1){
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        //2.2创建更新对象
        Article updateArticle = new Article();
        updateArticle.setId(id);
        updateArticle.setDeleteState((byte) 1);
        //2.3更新删除信息
        int row = articleMapper.updateByPrimaryKeySelective(updateArticle);
        if (row != 1) {
            log.warn(ResultCode.ERROR_SERVICES.toString());
            throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));
        }
        //3.板块贴子数-1
        boardService.subOneArticleCountByBoardId(article.getBoardId());
        //4.用户贴子数-1
        userService.subOneArtcleCountByUserId(article.getUserId());
       //5.打印日志
        log.info("用户id: " +article.getUserId() +
                "删除帖子成功" + "板块id: " +
                article.getBoardId() + "帖子id: " + article.getId());

    }

    @Override
    public void addReplyCountById(Long id) {
        //1.参数校验
        if (id == null || id <= 0) {
            //打印信息
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        //2.更新帖子信息
        //2.1查新帖子信息
        Article article = articleMapper.selectByPrimaryKey(id);
        if(article == null || article.getDeleteState() == 1){
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        //封贴
        if(article.getState() == 1){
            log.warn(ResultCode.FAILED_FORBIDDEN.toString());
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_FORBIDDEN));
        }
        //2.2创建更新对象
        Article updateArticle = new Article();
        updateArticle.setId(id);
        updateArticle.setReplyCount(article.getReplyCount() + 1);
        //2.3更新添加信息
        int row = articleMapper.updateByPrimaryKeySelective(updateArticle);
        if (row != 1) {
            log.warn(ResultCode.ERROR_SERVICES.toString());
            throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));
        }
    }

    @Override
    public List<Article> selectArticleByUserId(Long userId) {
        //1.参数校验
        if (userId == null || userId <= 0) {
            //打印信息
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        //2.查询用户状态
        User user = userService.selectById(userId);
        if(user == null || user.getState() == 1){
            log.warn(ResultCode.FAILED_FORBIDDEN.toString());
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_FORBIDDEN));
        }
        if(user.getDeleteState() == 1){
            log.warn(ResultCode.FAILED_USER_NOT_EXISTS.toString());
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_USER_NOT_EXISTS));
        }
        //3.查询帖子列表
        List<Article> articleList = articleMapper.selectArticleByUserId(userId);
        return articleList;
    }

    @Override
    public List<Article> searchArticle(String key) {
        //1.参数校验
        if(StringUtil.isEmpty(key)){
            //打印信息
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        //2.调用dao搜索关键字
        List<Article> articleList = new ArrayList<>();
        articleList = articleMapper.searchArticle(key);
        return articleList;
    }

    /**
     * 比较然后修改点赞关系
     * @param key 点赞关系key
     * @param cntKey 文章点赞数key
     * @param delta 用户要执行的操作，1点赞，0取消
     * @return
     */
    private Integer cau(String key,String cntKey,String delta){

//        //点赞 Lua 字符串
        final String LUA_THUMB =
                "local key   = KEYS[1] " +
                        "local cntKey= KEYS[2] " +
                        "local delta = ARGV[1] " +
                        "local flag  = redis.call('GET', key) " +
                        "if flag == nil then flag = '0' end " +
                        "if delta == '1' and flag == '0' then " +
                        "  redis.call('SET', key, '1') return redis.call('INCR', cntKey) " +
                        "elseif delta == '0' and flag == '1' then " +
                        "  redis.call('SET', key, '0') return redis.call('DECR', cntKey) " +
                        "else return nil end";


        Long newCnt = (Long) redisTemplate.execute(
                new DefaultRedisScript<>(LUA_THUMB, Long.class),
                Arrays.asList(key, cntKey),
                delta);
//        System.out.println("key:" + key);
//        System.out.println("delta:" + delta);
        System.out.println("newCnt:" + newCnt);
//        System.out.println("real in redis=[" + redisTemplate.opsForValue().get(key) + "]");
        return newCnt == null ? null : newCnt.intValue();
    }




}
